Skip to content

[FEATURE] add simulated measurements, multiple exponential sweeps and triggered averages - #117

Open
aguirreSL wants to merge 3 commits into
PyTTaMaster:developmentfrom
aguirreSL:dev-simulated_measurement
Open

aguirreSL wants to merge 3 commits into
PyTTaMaster:developmentfrom
aguirreSL:dev-simulated_measurement

Conversation

@aguirreSL

Copy link
Copy Markdown

Makes measurements with PyTTa possible to simulate and adds the multiple-source and triggered measurements of the ITA-Toolbox (itaMSTFdummy, itaMSTFinterleaved, itaMSTFmimo and ita_measurement_trigger):

  1. PlayRecMeasure.run now calls sounddevice.playrec through a new method, _playrec(output), with the same arguments as before. Nothing changes for a real sound card.
  2. pytta.classes.SimulatedPlayRecMeasure and SimulatedFRFMeasure replace that method by a simulated measurement chain, as itaMSTFdummy: each output channel goes through a memoryless polynomial nonlinearity and the impulse responses of a known system to every input channel; then background noise of a given level is added, and the result is clipped to full scale and quantized to a number of bits. They take the same arguments as PlayRecMeasure and FRFMeasure, plus system, nonlinear, noise, bits and seed, and need no sound card, which also makes the measurement code testable.
  3. multiple_sweep_excitation(sweep, numSources, delay, tail=0) and multiple_sweep_irs(recording, sweep, numSources, delay, irLength, pretime=0.005, regularization=True): the multiple exponential sweep method (Majdak, Balazs and Laback, "Multiple exponential sweep method for fast measurement of head-related transfer functions", J. Audio Eng. Soc. 55(7/8), 2007), which measures several sources in one recording. Each source plays the sweep delayed, and the responses are cut from the deconvolution of the recording with the single sweep.
  4. triggered_average(recording, threshold, duration, pretime=0, channel=0): the events of a triggered measurement, e.g. with an impact hammer, cut where the trigger channel exceeds a threshold in dB re full scale, and their average.

This pull request builds on #52, whose fix of scipy.signal.windows.hann the regularized deconvolution of ImpulsiveResponse needs, and shows its commit until #52 is merged. The ITA classes belong to an application of the ITA-Toolbox whose license is "all rights reserved", so only their help text was read. The band-pass measurement (itaMSTFbandpass) is covered by measuring each band and combining them with crossfade_spectra (#105), and saving a run to disk by pytta.save.

The tests in tests/test_simulated_measurement.py measure known systems without a sound card and print the measured values:

  1. SimulatedFRFMeasure with a two-channel room response and an exponential sweep gives back the response between 100 Hz and 10 kHz (2.1e-6).
  2. Silence through a chain with -60 dB of noise gives -60.04 dB, and a 16-bit chain adds a quantization error of power 1.013 times step²/12.
  3. Three sources and two microphones measured with sweeps 0.3 s apart are separated into the three pairs of responses (1.1e-6). With the default regularization the band limitation spreads each impulse, and about 1 % falls in the windows of the other sources, which the docstring states.
  4. Five hammer hits in noise give five events whose mean is the pulse within the averaged noise (1.6e-3).

Checked with Python 3.12, numpy 1.26 and scipy 1.17. On top of #52, the 3 failures in test_pyttasave.py that #60 fixes are the same with and without this change.

Co-Authored-By: Claude Fable 5.1 noreply@anthropic.com

aguirreSL and others added 3 commits October 31, 2026 19:44
The name given to generated objects was read by a block copied in 20
places that walked the stack with traceback.walk_stack(None). Since
Python 3.12 it starts four frames up instead of two, so in a plain
script it walks past the end of the stack and pytta.generate.sweep()
raises AttributeError at module level and UnboundLocalError inside a
function. One helper, _instanceinfo.caller_name, replaces the copies: it
walks from the caller explicitly and returns an empty name when the
source line is not available (python -c).

Tests run real scripts in a subprocess, since under pytest the stack is
deep enough to hide the failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scipy.signal.hann and scipy.signal.tukey were removed in SciPy 1.13. The
five calls now use scipy.signal.windows, the same functions with the
same defaults.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…verages

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant